
//
// 130423 - AH - Created.
//

package org.japril.jutil.xmloperations;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.japril.jutil.CharArrayPrintWriter;
import org.japril.jutil.logging.Log;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;

/**
 * <p>
 *   TODO
 * </p>
 * @author Arne Halvorsen (AH)
 */
public final class XmlOperations 
{
  private final List<XmlOperation> m_operations=new ArrayList<XmlOperation>();
  
  private boolean m_log=true;
  
  /**
   * <p>
   *   Constructor.
   * </p>
   */
  public XmlOperations(){}
  
  /**
   * <p>
   *   Tells if to do logging using
   *   {@link Log} API or not.
   * </p>
   * @param log {@code true} (default) to do, {@code false} if not to.
   */
  public void doLog(boolean log){ m_log=log; }
  
  /**
   * <p>
   *   Creates from  
   *   {@link #toString() string representation}.
   * </p>
   * <p>
   *   All operations will be
   *   {@link ProtoXmlOperation} instances.
   * </p>
   * @param s {@code String} representation.
   * @throws IOException If fails.
   */
  public XmlOperations(String s) throws IOException
  {
    if (s==null)
    {
      throw new NullPointerException("s");
    }
    
    InputStream is=null;
    try
    {
      is=new ByteArrayInputStream(s.getBytes());
      parse(is);
    }
    finally
    {
      if (is!=null) try { is.close(); } catch (IOException _){}   
    }
  }
  
  /**
   * <p>
   *   Creates from 
   *   {@link File} with 
   *   {@link #toString() string representation}.
   * </p>
   * <p>
   *   All operations will be
   *   {@link ProtoXmlOperation} instances.
   * </p>
   * @param f {@link File} to read.
   * @throws IOException If fails.
   */
  public XmlOperations(File f) throws IOException
  {
    if (f==null)
    {
      throw new NullPointerException("f");
    }
    
    InputStream is=null;
    try
    {
      is=new FileInputStream(f);
      parse(is);
    }
    finally
    {
      if (is!=null) try { is.close(); } catch (IOException _){}   
    }
  }
  
  /**
   * <p>
   *   Creates from stream with 
   *   {@link #toString() string representation}.
   * </p>
   * <p>
   *   All operations will be
   *   {@link ProtoXmlOperation} instances.
   * </p>
   * @param is Stream to read.
   * @throws IOException If fails.
   */
  public XmlOperations(InputStream is) throws IOException { parse(is); }
  
  /**
   * <p>
   *   Parses 
   *   {@link File} with 
   *   {@link #toString() string representation}.
   * </p>
   * @param is Stream to read.
   * @throws IOException If fails.
   */
  public void parse(File f) throws IOException
  {
    if (f==null)
    {
      throw new NullPointerException("f");
    }
   
    InputStream is=null;
    try
    {
      is=new FileInputStream(f);
      parse(is);
    }
    finally
    {
      if (is!=null) try { is.close(); } catch (IOException _){}
    }
  }
  
  /**
   * <p>
   *   Parses stream with 
   *   {@link #toString() string representation}.
   * </p>
   * @param is Stream to read.
   * @throws IOException If fails.
   */
  public void parse(InputStream is) throws IOException
  {
    if (is==null)
    {
      throw new NullPointerException("is");
    }
    
    ProtoXmlOperation o=null;
    
    BufferedReader br=new BufferedReader(new InputStreamReader(is));
    String line=br.readLine();
    while (line!=null)
    {
      if (line.startsWith("operation "))
      {
        StringTokenizer st=new StringTokenizer(line);
        st.nextToken();
        if (st.hasMoreTokens())
        {
          String name=st.nextToken();
          o=new ProtoXmlOperation(name);
          addOperation(o);
        }
      }
      else if (line.startsWith("path"))
      {
        if (o!=null)
        {
          StringTokenizer st=new StringTokenizer(line);
          st.nextToken();
          if (st.hasMoreTokens())
          {
            String path=st.nextToken();
            o.addPath(path);
          }  
        }
      }
      
      line=br.readLine();
    }
  }
  
  /**
   * <p>
   *   Adds
   *   {@link XmlOperation}.
   * </p>
   * @param oper {@code XmlOperation} to add-
   */
  public void addOperation(XmlOperation oper)
  {
    if (oper==null)
    {
      throw new NullPointerException("oper");
    }
    
    m_operations.add(oper);
  }
  
  /**
   * <p>
   *   Removes
   *   {@link XmlOperation}.
   * </p>
   * @param idx Index of operation to remove.
   * @throws IndexOutOfBoundsException If {@code idx<0 || idx>=size()}.
   */
  public void removeOperation(int idx){ m_operations.remove(idx); }
  
  /**
   * <p>
   *   Removes
   *   {@link XmlOperation}.
   * </p>
   * @param oper {@code XmlOperation} to remove.
   * @return {@code true} if removed. {@code false} if not found.
   */
  public boolean removeOperation(XmlOperation oper)
  {
    if (oper==null)
    {
      throw new NullPointerException("oper");
    }
    
    return m_operations.remove(oper);
  }
  
  /**
   * <p>
   *   Gets number of 
   *   {@link XmlOperation} {@code this} has.
   * </p>
   * @return Count.
   */
  public int size(){ return m_operations.size(); }
  
  /**
   * <p>
   *   Gets
   *   {@link XmlOperation}.
   * </p>
   * @param idx Index of
   *            {@code XmlOperation} to get.
   * @return {@code XmlOperation}.
   * @throws IndexOutOfBoundsException If {@code idx<0 || idx>=size()}.
   */
  public XmlOperation getOperation(int idx){ return m_operations.get(idx); }
  
  /**
   * <p>
   *   Gets index of
   *   {@link XmlOperation}.
   * </p>
   * @param o {@link XmlOperation}.
   * @return Index or {@code -1} if not found.
   */
  public int getIndexOfOperation(XmlOperation o)
  {
    if (o==null)
    {
      throw new NullPointerException("o");
    }
    
    return m_operations.indexOf(o);
  }
  
  /**
   * <p>
   *   {@link XmlOperation#perform(String) Perform} 
   *   {@link XmlOperation}s which paths are found in given XML.
   * </p>
   * @param xml XML.
   * @return Report on performed operations.
   * @throws IOException If fails to parse XML or an operation fails.
   */
  public String perform(String xml) throws IOException
  {
    if (xml==null)
    {
      throw new NullPointerException("null");
    }
    
    CharArrayPrintWriter capw=new CharArrayPrintWriter();
    capw.println("Operations performed:");
    
    ByteArrayInputStream bais=new ByteArrayInputStream(xml.getBytes());
    try
    {
      DocumentBuilderFactory domFactory=DocumentBuilderFactory.newInstance();
      domFactory.setNamespaceAware(true); // never forget this!
      DocumentBuilder builder=domFactory.newDocumentBuilder();
      Document doc=builder.parse(bais);
    
      XPath path=XPathFactory.newInstance().newXPath();
      for (XmlOperation xo : m_operations)
      {
        boolean perform=false;
        
        int n=xo.getPathCount();
        for (int i=0; i<n && !perform; i++)
        {
          String p=xo.getPath(i);
          Node node=(Node)path.evaluate(p, doc, XPathConstants.NODE);
          perform=(node!=null);
          
          if (perform)
          {
            if (m_log)
            {
              Log.log(this, "perform", "path: "+p+" exists so perform: "+
                xo.getName());
            }
          }
        }
        
        if (perform) 
        {
          capw.println("  "+xo.getName());
          xo.perform(xml);
        }
        else
        {
          if (m_log) Log.log(this, "perform", xo.getName()+" NOT performed");
        }
      }
      
      return capw.toString();
    }
    catch (ParserConfigurationException pcx)
    {
      IOException iox=new IOException();
      iox.initCause(pcx);
      throw iox;
    }
    catch (SAXException saxx)
    {
      IOException iox=new IOException();
      iox.initCause(saxx);
      throw iox;
    }
    catch (XPathExpressionException xpex)
    {
      IOException iox=new IOException();
      iox.initCause(xpex);
      throw iox;
    }
  }
  
  @Override
  public String toString()
  {
    CharArrayPrintWriter capw=new CharArrayPrintWriter();
    
    StringBuilder sb=new StringBuilder();
    
    for (XmlOperation oper : m_operations)
    {
      sb.setLength(0);
      sb.append("operation ").append(oper.getName());
      capw.println(sb);
      
      int n=oper.getPathCount();
      for (int i=0; i<n; i++)
      {
        String p=oper.getPath(i);
        sb.setLength(0);
        sb.append("path ").append(p);
        capw.println(sb);
      }
    }
    
    return capw.toString();
  }
  
  /**
   * <p>
   *   TODO
   * </p>
   * @return
   */
  public String docToString()
  {
    CharArrayPrintWriter capw=new CharArrayPrintWriter();
    
    for (XmlOperation curr : m_operations)
    {
      String desc=curr.getDesc();
      if (desc!=null)
      {
        desc=desc.trim();
        if (desc.length()>0)
        {
          capw.println(curr.getName());
          capw.println(desc);
          capw.println("<<desc-end>>");
        }
      }
    }
    
    return capw.toString();
  }
  
  public static Map<String, String> parseDesc(String txt)
  {
    if (txt==null)
    {
      throw new NullPointerException("txt");
    }
    
    Map<String, String> retVal=new HashMap<String, String>();
    
    try
    {
      ByteArrayInputStream bais=new ByteArrayInputStream(txt.getBytes());
      BufferedReader br=new BufferedReader(new InputStreamReader(bais));
      String l=br.readLine();
      while (l!=null)
      {
        l=l.trim();
        if (l.length()>0)
        {
          String name=l;
          CharArrayPrintWriter capw=new CharArrayPrintWriter();
          l=br.readLine();
          boolean done=false;
          while (l!=null && !done)
          {
            l=l.trim();
            if (l.equals("<<desc-end>>"))
            {
              String desc=capw.toString();
              desc=(desc==null) ? "" : desc.trim();
              if (desc.length()>0)
              {
                retVal.put(name, desc);
              }
              done=true;
            }
            else
            {
              capw.println(l);
              l=br.readLine();
            }
          }
        }
        
        l=br.readLine();  
      }
    }
    catch (IOException _){}
    
    return retVal;
  }
  
  public void setDescriptions(Map<String, String> descs)
  {
    if (descs==null)
    {
      throw new NullPointerException("descs");
    }
    
    for (XmlOperation curr : m_operations)
    {
      if (curr instanceof AbstractXmlOperation)
      {
        AbstractXmlOperation xao=(AbstractXmlOperation)curr;
        String name=xao.getName();
        String desc=descs.get(name);
        desc=(desc==null) ? "" : desc.trim();
        if (desc.length()>0)
        {
          xao.setDesc(desc);
        }
      }
    }
  }
  
  /**
   * <p>
   *   Simple test program
   * </p>
   * @param args
   */
  public static void main(String[] args) 
  {
    try
    {
      String xml="<?xml version='1.0'?>"+
                 "<a>"+
                   "<b/>"+
                 "</a>";
    
      XmlOperations opers=new XmlOperations();
      ProtoXmlOperation o=new ProtoXmlOperation("o1");
      o.addPath("/a/b");
      opers.addOperation(o);
    
      opers.perform(xml);
      
      System.exit(0);
    }
    catch (IOException iox)
    {
      iox.printStackTrace();
      System.exit(-1);
    }
  }
  
}
